import os
import random
from typing import Optional, Union
from typing_extensions import Literal
from openpyxl import load_workbook
import pandas as pd
from docxtpl import DocxTemplate
from ._types import NotGiven, NOT_GIVEN
from ._exceptions import (
    SRCTypeError, SRCZeroColumnError, SRCZeroRowError, ColsNotInSRCError,
    TemplateTypeError,
)
__all__ = [
    "getFilter"
]

class Filler:

    def __init__(
        self, 
        src:pd.DataFrame, 
        template:str, 
        output_dir: Optional[str] | NotGiven = NOT_GIVEN,
        tpl_type: Union[str, Literal["xlsx", "docx"]] = "xlsx",
        cols_for_fnames: Optional[list[str]] | NotGiven = NOT_GIVEN,
        # orderid_colname:str='oid',
        # unitname_colname:str='unit_name',
        # unitid_colname:str='unit_id',
        ):
        '''
        param src: pd.DataFrame, the data source collect, its columns must include column names specified by unitname_colname & unitid_colname
        param template: docx|xlsx, the template for filling in
        param output_dir: the directory for storing output files
        param orderid_colname: the column name of order id filed in src
        param unitname_colname: the column name of unit name filed in src
        param unitid_colname: the column name of unit id filed in src
        '''
        self.src = src
        self.template = template
        self.output_dir = output_dir
        self.tpl_type = tpl_type
        self.cols_for_fnames = cols_for_fnames
        # self.orderid_colname = orderid_colname
        # self.unitname_colname = unitname_colname
        # self.unitid_colname = unitid_colname

    def bulk_fill(self):
        for _, row in self.src.iterrows():
            order = f"{row[self.orderid_colname]:03}"
            unit_name = row[self.unitname_colname]
            unit_id = row[self.unitid_colname]
            out_file = self._gen_file_name_(order, unit_name, unit_id)
            self._fill_(row, self.template, out_file)

    def _fill_(self, data:pd.Series, template:str, out_file:str):
        pass

    def _gen_file_name_(self, order:str, unit_name:str, unit_id:str)->str:
        pass
    
class Filler_docx(Filler):
    
    def _fill_(self, data:pd.Series, template:str, out_file:str):
        '''
        在 template 的 plain 文本中或 table 文本中存在类似 {{A}} 这样的占位符
        将 data 中索引值 与 占位符对应的值替换占位符，全部替换完后，保存为 out_file 指定的路径
        param data: pd.Series, 填充数据源
        param template: docx文件, 填充模板
        param out_file: docx文件名, 填充后输出路径
        '''
        if not os.path.exists(template):
            raise FileNotFoundError(f'{template} 文件不存在')
        # 读取 docx 文件，获取文本内容
        tpl = DocxTemplate(template)          # 加载模板文件
        context = data.to_dict()           # 将数据源转换为字典
        tpl.render(context)               # 对模板文件进行渲染
        tpl.save(out_file)               # 保存结果文件        

    def _gen_file_name_(self, order:str, unit_name:str, unit_id:str)->str:
        file = f"{order}_{unit_name}_{unit_id}.docx"
        return merge_dir_file(self.output_dir, file)
    
class Filler_xlsx(Filler):
    
    def _fill_(self, data:pd.Series, template:str, out_file:str):
        '''
        在 template 的 sheets 中存在类似 {{A}} 这样的占位符
        将 data 中索引值 与 占位符对应的值替换占位符，全部替换完后，保存为 out_file 指定的路径
        param data: pd.Series, 填充数据源
        param template: xlsx文件, 填充模板
        param out_file: xlsx文件名, 填充后输出路径
        '''
        book = load_workbook(template)    # 加载模板文件

        # 遍历所有工作表
        for sheet in book.worksheets:

            # 遍历工作表中的所有单元格
            for row in sheet.iter_rows():
                for cell in row:
                    # 如果单元格中的值是字符串，并且含有占位符（例如 '{{A}}'）
                    if isinstance(cell.value, str) and '{{' in cell.value:
                        # 提取出占位符（例如 'A'）
                        key = cell.value.strip('{}')
                        # 如果在数据源中找到占位符所对应的数据，就将占位符替换为该数据
                        if key in data:
                            if str(data[key])!='nan':
                                
                                cell.value = cell.value.replace('{{' + key + '}}', str(data[key]))
                                cell.value = float(cell.value) if is_number(cell.value) else cell.value
                            elif str(data[key])=='nan':
                                cell.value = None

        # 保存结果文件
        book.save(out_file)

    def _gen_file_name_(self, order:str, unit_name:str, unit_id:str)->str:
        file = f"{order}_{unit_name}_{unit_id}.xlsx"
        return merge_dir_file(self.output_dir, file)

def getFilter(
        src: pd.DataFrame,
        template: str, 
        output_dir: Optional[str] | NotGiven = NOT_GIVEN,
        tpl_type: Union[str, Literal["xlsx", "docx"]] = "xlsx",
        cols_for_fnames: Optional[list[str]] | NotGiven = NOT_GIVEN,
    ):

    # check out whether src is valid
    _check_src(src, cols_for_fnames)
    # check out whether tpl_type is valid
    _check_tpltype(tpl_type)

    # assign default output dir if not given
    if output_dir is NOT_GIVEN:
        output_dir = 'default_output_dir'
    
    # set output file names by cols_for_fnames param
    if cols_for_fnames is NOT_GIVEN:
        src['luowen_fname'] = [str(row) for row in range(1, len(src) + 1)]
        max = src['luowen_fname'].apply(len).max()
        src['luowen_fname'] = src['luowen_fname'].str.rjust(max,'0')
    else:
        src['luowen_fname'] = src[cols_for_fnames].apply(lambda row: '_'.join([str(v) for v in row]), axis=1)

    


    return filler

def _check_src(src:pd.DataFrame, cols_for_fnames: Optional[list[str]] | NotGiven = NOT_GIVEN,):
    
    if not isinstance(src, pd.DataFrame):
        raise SRCTypeError('src must is DataFrame object of pandas.')
    
    if src.shape[1]==0:
        raise SRCZeroColumnError('src must have at least one column.')
    
    if src.shape[0]==0:
        raise SRCZeroRowError('src must have at least one row.')
    
    if isinstance(cols_for_fnames, list):
        for row in cols_for_fnames:
            if  row not in src.columns:
                raise ColsNotInSRCError(f'{row} is not in src columns.')

def _check_tpltype(tpl_type):
    if tpl_type not in  ["docx", "xlsx"]:
        raise TemplateTypeError("template type must be 'docx' or 'xlsx'")
    
def get_timestr()->str:
    '''
    生成日期时间字符串
    >>> get_timestr()
    202312190844
    '''
    return pd.Timestamp.now().strftime('%Y%m%d%H%M%S')

def create_output_dir(pre:str):
    '''在当前目录创建以 pre 为文件名的子目录，并返回目录名'''
    dir = pre + '_' + get_timestr()
    if not os.path.exists(dir):
        os.mkdir(dir)
    return dir

def merge_dir_file(dir, file):
    '''
    dir: 当前目录下子目录名
    file: 要放在 dir 目录的文件名
    生成完整路径
    ''' 
    return os.path.join(dir, file)

def is_number(s):
    try:
        float(s)
        return True
    except ValueError:
        return False

def rand_float(start:float=-1, end:float=1, point:int=2)->float:
    '''
    generate a random floating-point number between start and end.
    param start: float, lower boundary.
    param end: float, higher boundary.
    param point: int, decimal places.
    return : float, random float between start and end.
    ''' 

    return round(
        random.uniform(start, end),
        point
    )



